package com.atguigu.tingshu.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.util.PinYinUtils;
import com.atguigu.tingshu.model.album.AlbumAttributeValue;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.AttributeValueIndex;
import com.atguigu.tingshu.model.search.SuggestIndex;
import com.atguigu.tingshu.search.repository.AlbumInfoRepository;
import com.atguigu.tingshu.search.repository.SuggestInfoRepository;
import com.atguigu.tingshu.search.service.ItemService;
import com.atguigu.tingshu.user.client.UserInfoFeignClient;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.suggest.Completion;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.*;
import java.util.concurrent.*;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class ItemServiceImpl implements ItemService {

    @Autowired
    private AlbumInfoRepository albumInfoRepository;

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Autowired
    private SuggestInfoRepository suggestInfoRepository;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    @Autowired
    private StringRedisTemplate redisTemplate;


    //引入线程池，来优化上传文档到es这段业务的速度
    static ExecutorService executorService = null;

    //静态代码块：初始化线程池
    //复习一下静态代码块：静态代码块中的内容会作为类的本身一部分，在类被加载的时候执行，只执行一次
    static {
        executorService = Executors.newFixedThreadPool(4);
    }


    /**
     * 现在使用优化手段二：
     * 第一次上传:223ms
     * 第二次上传:35ms
     * new Thread方式手动创建线程
     * new Thread + futuretask + callable + join
     * 或者 new Thread + runable + join +CMap
     */
    //创建一个线程安全的map对象，用来存储某些线程需要的数据
    /*Map cMap = new ConcurrentHashMap<String, Long>();

    @Override
    @SneakyThrows
    public void upperAlbum(Long albumId) {
        long startTime = System.currentTimeMillis();
        //1.创建文档对象
        AlbumInfoIndex index = new AlbumInfoIndex();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //2.4 category表
                //根据专辑id，获取该专辑对应的一、二、三级目录的id
                Result<BaseCategoryView> categoryViewResult = albumInfoFeignClient.getCategoryView(albumId);
                BaseCategoryView categoryView = categoryViewResult.getData();
                Assert.notNull(categoryView, "[rpc]远程调用album微服务的getCategoryView方法失败");

                //为AlbumInfoIndex对象的分类id属性赋值
                index.setCategory1Id(categoryView.getCategory1Id());
                index.setCategory2Id(categoryView.getCategory2Id());
                index.setCategory3Id(categoryView.getCategory3Id());
            }
        });

        //启动t1线程
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //2.5 album_stat表
                Result<AlbumStatVo> albumStatVoResult = albumInfoFeignClient.getAlbumStatVoByAlbumId(albumId);
                AlbumStatVo albumStatVo = albumStatVoResult.getData();
                Assert.notNull(albumStatVo, "[rpc]远程调用album微服务的getAlbumStatVoByAlbumId方法失败");

                //获取四个维度的播放量、订阅量、购买量、评论量，并赋值给AlbumInfoIndex对象
                Integer playStatNum = albumStatVo.getPlayStatNum();
                index.setPlayStatNum(playStatNum);
                Integer subscribeStatNum = albumStatVo.getSubscribeStatNum();
                index.setSubscribeStatNum(subscribeStatNum);
                Integer buyStatNum = albumStatVo.getBuyStatNum();
                index.setBuyStatNum(buyStatNum);
                Integer commentStatNum = albumStatVo.getCommentStatNum();
                index.setCommentStatNum(commentStatNum);

                //2.5 热度值
                //Double hotScore = playStatNum * 0.4 + subscribeStatNum * 0.1 + buyStatNum * 0.3 + commentStatNum * 0.2;
                Double hotScore = new Random().nextDouble();
                index.setHotScore(hotScore);
            }
        });

        //启动t2线程
        t2.start();

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //2.给文档对象赋值
                //2.1 album_info表
                Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(albumId);
                //通过FeignClient，远程调用album微服务，根据专辑id获取专辑信息
                AlbumInfo albumInfo = albumInfoResult.getData();
                Assert.notNull(albumInfo, "[rpc]远程调用album微服务的getAlbumInfo方法失败");

                //获取专辑信息对象，手动为AlbumInfoIndex对象赋值
                index.setId(albumInfo.getId());
                index.setAlbumTitle(albumInfo.getAlbumTitle());
                index.setAlbumIntro(albumInfo.getAlbumIntro());
                index.setCoverUrl(albumInfo.getCoverUrl());
                index.setIncludeTrackCount(albumInfo.getIncludeTrackCount());
                index.setIsFinished(String.valueOf(albumInfo.getIsFinished()));
                index.setPayType(albumInfo.getPayType());
                //将创建时间设置为当前时间
                index.setCreateTime(new Date());

                //2.2 album_attribute_value表
                //我们获取的专辑信息对象中，包含了专辑属性值集合，我们需要将专辑属性值集合中的数据，封装到AlbumInfoIndex对象中
                List<AlbumAttributeValue> albumAttributeValueVoList = albumInfo.getAlbumAttributeValueVoList();
                //若专辑属性值集合不为空，我们就将集合中的每个属性值对象转换为AttributeValueIndex对象，并组成集合
                if (albumAttributeValueVoList != null) {
                    List<AttributeValueIndex> list = albumAttributeValueVoList.stream().map(vo -> {
                        AttributeValueIndex attributeValueIndex = new AttributeValueIndex();
                        attributeValueIndex.setAttributeId(vo.getAttributeId());
                        attributeValueIndex.setValueId(vo.getValueId());
                        return attributeValueIndex;
                    }).toList();
                    //为对象的AttributeValueIndexList属性赋值
                    index.setAttributeValueIndexList(list);
                }
                cMap.put("userId", albumInfo.getUserId());
            }
        });

        //启动t3线程
        t3.start();
        //在这里阻塞一下线程:t3.join(),因为接下来要执行的t4线程需要用到cMap中的userId，
        // 然而t3如果没有执行完，那么t4线程是无法获取到cMap中的userId的
        t3.join();

        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                Long userId = (Long) cMap.get("userId");
                //2.3 user_info表
                //通过远程调用访问user微服务，根据上面我们的专辑信息对象，获取用户id，根据用户id获取用户信息
                Result<UserInfoVo> userInfoResult = userInfoFeignClient.getUserInfoVo(userId);
                UserInfoVo userInfo = userInfoResult.getData();
                Assert.notNull(userInfo, "[rpc]远程调用user微服务的getUserInfoVo方法失败");

                //为AlbumInfoIndex对象的发布者属性赋值，即用户的昵称
                index.setAnnouncerName(userInfo.getNickname());

            }
        });

        //启动t4线程
        t4.start();

        //3.存入es
        //在存入之前，必须要保证index对象被全体赋值完毕，因此在这里每个线程都阻塞一下
        t1.join();
        t2.join();
        t4.join();
        long endTime = System.currentTimeMillis();
        AlbumInfoIndex flag = albumInfoRepository.save(index);
        log.info("专辑:{}存储到ElasticSearch库中，状态:{},具体耗时:{}", albumId, flag != null ? "success" : "fail", endTime - startTime);

    }*/


    /**
     * 上架专辑
     * 优化手段一：线程池 + CountDownLatch
     * 第一次上传：238ms
     * 第二次上传：27 ms  看样子是我的电脑性能太高了，没有太大差别哈哈哈
     *
     * @param albumId 专辑id
     */
    @Override
    public void upperAlbum(Long albumId) {
        //0.设置一个countdownLatch，用于给线程池一个计数器，当线程池中的线程执行完毕后，计数器减1，当计数器为0时，线程池中的线程全部执行完毕，程序继续往下执行
        //简单点说就是搞一把计数锁
        /**
         * 这里有一个知识点：
         * 【在当前这个业务场景中】
         * countdownLatch必须设置在方法内，作为局部变量
         * 如果放在方法外部，类当中，作为成员变量，那么就会遇到错误
         * 成员变量是存放在堆内存当中的，堆是内存共享的，如果将其放在堆内存当中
         * 其他线程就会共享这个计数器锁，那么两个线程都会去操控这个countdownlatch的值
         * 只要其减到0了，就会导致程序提前放开，不按照我们的要求执行任务
         * */

        CountDownLatch countDownLatch = new CountDownLatch(4);

        long startTime = System.currentTimeMillis();
        //1.创建文档对象
        AlbumInfoIndex index = new AlbumInfoIndex();

        Future<Long> future = executorService.submit(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                //2.给文档对象赋值
                //2.1 album_info表
                Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(albumId);
                //通过FeignClient，远程调用album微服务，根据专辑id获取专辑信息
                AlbumInfo albumInfo = albumInfoResult.getData();
                Assert.notNull(albumInfo, "[ItemServiceImpl]远程调用album微服务的getAlbumInfo方法失败");

                //获取专辑信息对象，手动为AlbumInfoIndex对象赋值
                index.setId(albumInfo.getId());
                index.setAlbumTitle(albumInfo.getAlbumTitle());
                index.setAlbumIntro(albumInfo.getAlbumIntro());
                index.setCoverUrl(albumInfo.getCoverUrl());
                index.setIncludeTrackCount(albumInfo.getIncludeTrackCount());
                index.setIsFinished(String.valueOf(albumInfo.getIsFinished()));
                index.setPayType(albumInfo.getPayType());
                //将创建时间设置为当前时间
                index.setCreateTime(new Date());

                //2.2 album_attribute_value表
                //我们获取的专辑信息对象中，包含了专辑属性值集合，我们需要将专辑属性值集合中的数据，封装到AlbumInfoIndex对象中
                List<AlbumAttributeValue> albumAttributeValueVoList = albumInfo.getAlbumAttributeValueVoList();
                //若专辑属性值集合不为空，我们就将集合中的每个属性值对象转换为AttributeValueIndex对象，并组成集合
                if (albumAttributeValueVoList != null) {
                    List<AttributeValueIndex> list = albumAttributeValueVoList.stream().map(vo -> {
                        AttributeValueIndex attributeValueIndex = new AttributeValueIndex();
                        attributeValueIndex.setAttributeId(vo.getAttributeId());
                        attributeValueIndex.setValueId(vo.getValueId());
                        return attributeValueIndex;
                    }).toList();
                    //为对象的AttributeValueIndexList属性赋值
                    index.setAttributeValueIndexList(list);
                }
                //计数器减1
                countDownLatch.countDown();
                //callable的返回值，我们返回userId，用于远程调用user模块来获取用户信息
                return albumInfo.getUserId();
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                Long userId = null;
                try {
                    userId = future.get();
                    //2.3 user_info表
                    //通过远程调用访问user微服务，根据上面我们的专辑信息对象，获取用户id，根据用户id获取用户信息
                    Result<UserInfoVo> userInfoResult = userInfoFeignClient.getUserInfoVo(userId);
                    UserInfoVo userInfo = userInfoResult.getData();
                    Assert.notNull(userInfo, "[ItemServiceImpl]远程调用user微服务的getUserInfoVo方法失败");

                    //为AlbumInfoIndex对象的发布者属性赋值，即用户的昵称
                    index.setAnnouncerName(userInfo.getNickname());
                    //计数器减1
                    countDownLatch.countDown();
                } catch (Exception e) {
                    log.error("当前线程阻塞期间发生了异常:{}", e.getMessage());
                }
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //2.4 category表
                //根据专辑id，获取该专辑对应的一、二、三级目录的id
                Result<BaseCategoryView> categoryViewResult = albumInfoFeignClient.getCategoryView(albumId);
                BaseCategoryView categoryView = categoryViewResult.getData();
                Assert.notNull(categoryView, "[ItemServiceImpl]远程调用album微服务的getCategoryView方法失败");

                //为AlbumInfoIndex对象的分类id属性赋值
                index.setCategory1Id(categoryView.getCategory1Id());
                index.setCategory2Id(categoryView.getCategory2Id());
                index.setCategory3Id(categoryView.getCategory3Id());

                //计数器减1
                countDownLatch.countDown();
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //2.5 album_stat表
                Result<AlbumStatVo> albumStatVoResult = albumInfoFeignClient.getAlbumStatVoByAlbumId(albumId);
                AlbumStatVo albumStatVo = albumStatVoResult.getData();
                Assert.notNull(albumStatVo, "[ItemServiceImpl]远程调用album微服务的getAlbumStatVoByAlbumId方法失败");

                //获取四个维度的播放量、订阅量、购买量、评论量，并赋值给AlbumInfoIndex对象
                Integer playStatNum = albumStatVo.getPlayStatNum();
                index.setPlayStatNum(playStatNum);
                Integer subscribeStatNum = albumStatVo.getSubscribeStatNum();
                index.setSubscribeStatNum(subscribeStatNum);
                Integer buyStatNum = albumStatVo.getBuyStatNum();
                index.setBuyStatNum(buyStatNum);
                Integer commentStatNum = albumStatVo.getCommentStatNum();
                index.setCommentStatNum(commentStatNum);
                //2.5 热度值
                //Double hotScore = playStatNum * 0.4 + subscribeStatNum * 0.1 + buyStatNum * 0.3 + commentStatNum * 0.2;
                Double hotScore = new Random().nextDouble();
                index.setHotScore(hotScore);

                //计数器减1
                countDownLatch.countDown();
            }
        });

        //3.存入es
        //在这里设置计数器锁，若计数器的值没有变为0，就阻塞线程，若为0，就解除阻塞
        try {

            countDownLatch.await();
            long endTime = System.currentTimeMillis();
            AlbumInfoIndex flag = albumInfoRepository.save(index);
            log.info("专辑:{}存储到ElasticSearch库中，状态:{},具体耗时:{}", albumId, flag != null ? "success" : "fail", endTime - startTime);

        } catch (InterruptedException e) {

            log.error("当前线程阻塞期间发生了异常:{}", e.getMessage());

        }

        //4.存入另外一个文档 --> 提示词文档，用于搜索检索数据
        String albumTitle = index.getAlbumTitle();
        SuggestIndex suggestIndex = new SuggestIndex();
        suggestIndex.setId(albumId.toString());
        suggestIndex.setTitle(albumTitle);
        suggestIndex.setKeyword(new Completion(new String[]{albumTitle}));
        suggestIndex.setKeywordPinyin(new Completion(new String[]{PinYinUtils.toHanyuPinyin(albumTitle)}));
        suggestIndex.setKeywordSequence(new Completion(new String[]{PinYinUtils.getFirstLetter(albumTitle)}));

        suggestInfoRepository.save(suggestIndex);

    }


    /**
     * 下架专辑
     *
     * @param albumId 专辑id
     */
    @Override
    public void lowerAlbum(Long albumId) {
        albumInfoRepository.deleteById(albumId);
    }


    /**
     * 根据当前的一级分类id和分类维度来获取排行榜数据
     * ps : 从redis中
     *
     * @param category1Id 一级分类id
     * @param dimension   分类维度
     * @return 排行榜数据
     */
    @Override
    public List<AlbumInfoIndexVo> findRankingList(Long category1Id, String dimension) {
        String bigKey = RedisConstant.RANKING_KEY_PREFIX + category1Id;
        String resultStr = (String) redisTemplate.opsForHash().get(bigKey, dimension);
        return JSONObject.parseArray(resultStr, AlbumInfoIndexVo.class);
    }


    /**
     * 排行榜数据预热
     */
    @Override
    @SneakyThrows
    public void warmUpRanking() {
        //1.第一步，获取一级分类id的集合
        Result<List<Long>> category1IdListResult = albumInfoFeignClient.getCategory1IdList();
        List<Long> category1IdList = category1IdListResult.getData();
        Assert.notNull(category1IdList, "[ItemServiceImpl]远程调用album微服务的getCategory1IdList方法失败");

        //2.遍历一级分类id集合，根据一级分类id去es中，根据分类维度来进行查询
        String[] dimensionArray = {"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
        //遍历一级id集合
        for (Long c1Id : category1IdList) {
            //根据一级id集合，查询每个一级id下的四个维度的排行数据
            for (String dimension : dimensionArray) {
                //构造查询排行榜请求
                SearchRequest.Builder requestBuilder = new SearchRequest.Builder();
                SearchRequest request = requestBuilder
                        .index("albuminfo")
                        .query(qb -> qb
                                .term(tqb -> tqb.field("category1Id")
                                        .value(c1Id)))
                        .sort(sob -> sob
                                .field(fsb -> fsb
                                        .field(dimension)
                                        .order(SortOrder.Desc)))
                        .size(10)
                        .build();
                //获取排行榜具体数据，并且存入redis中
                SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(request, AlbumInfoIndex.class);
                ArrayList<AlbumInfoIndex> dataList = new ArrayList<>();
                for (Hit<AlbumInfoIndex> hit : response.hits().hits()) {
                    //获取到具体的数据
                    AlbumInfoIndex data = hit.source();
                    dataList.add(data);
                }

                //将数据存入redis当中
                //以Hash的数据类型存入，大Key为 固定前缀 + 一级分类id，小Key为dimension
                String bigKey = RedisConstant.RANKING_KEY_PREFIX + c1Id;
                log.info("[排行榜数据预热 - redis] 存入一级id:{} 维度{} 的排行榜数据", c1Id, dimension);
                redisTemplate.opsForHash().put(bigKey, dimension, JSONObject.toJSONString(dataList));
            }
        }
    }

    /*
    * 传统方式：[单线程]
    * 第一次存储耗时：231ms
    * 第二次存储：36ms
    * 平均耗时35ms左右
    @Override
    public void upperAlbum(Long albumId) {

        long startTime = System.currentTimeMillis();
        //1.创建文档对象
        AlbumInfoIndex index = new AlbumInfoIndex();

        //2.给文档对象赋值
        //2.1 album_info表
        Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(albumId);
        //通过FeignClient，远程调用album微服务，根据专辑id获取专辑信息
        AlbumInfo albumInfo = albumInfoResult.getData();
        Assert.notNull(albumInfo,"[rpc]远程调用album微服务的getAlbumInfo方法失败");

        //获取专辑信息对象，手动为AlbumInfoIndex对象赋值
        index.setId(albumInfo.getId());
        index.setAlbumTitle(albumInfo.getAlbumTitle());
        index.setAlbumIntro(albumInfo.getAlbumIntro());
        index.setCoverUrl(albumInfo.getCoverUrl());
        index.setIncludeTrackCount(albumInfo.getIncludeTrackCount());
        index.setIsFinished(String.valueOf(albumInfo.getIsFinished()));
        index.setPayType(albumInfo.getPayType());
        //将创建时间设置为当前时间
        index.setCreateTime(new Date());

        //2.2 album_attribute_value表
        //我们获取的专辑信息对象中，包含了专辑属性值集合，我们需要将专辑属性值集合中的数据，封装到AlbumInfoIndex对象中
        List<AlbumAttributeValue> albumAttributeValueVoList = albumInfo.getAlbumAttributeValueVoList();
        //若专辑属性值集合不为空，我们就将集合中的每个属性值对象转换为AttributeValueIndex对象，并组成集合
        if (albumAttributeValueVoList != null){
            List<AttributeValueIndex> list = albumAttributeValueVoList.stream().map(vo -> {
                AttributeValueIndex attributeValueIndex = new AttributeValueIndex();
                attributeValueIndex.setAttributeId(vo.getAttributeId());
                attributeValueIndex.setValueId(vo.getValueId());
                return attributeValueIndex;
            }).toList();
            //为对象的AttributeValueIndexList属性赋值
            index.setAttributeValueIndexList(list);
        }

        //2.3 user_info表
        //通过远程调用访问user微服务，根据上面我们的专辑信息对象，获取用户id，根据用户id获取用户信息
        Result<UserInfoVo> userInfoResult = userInfoFeignClient.getUserInfoVo(albumInfo.getUserId());
        UserInfoVo userInfo = userInfoResult.getData();
        Assert.notNull(userInfo,"[rpc]远程调用user微服务的getUserInfoVo方法失败");

        //为AlbumInfoIndex对象的发布者属性赋值，即用户的昵称
        index.setAnnouncerName(userInfo.getNickname());

        //2.4 category表
        //根据专辑id，获取该专辑对应的一、二、三级目录的id
        Result<BaseCategoryView> categoryViewResult = albumInfoFeignClient.getCategoryView(albumId);
        BaseCategoryView categoryView = categoryViewResult.getData();
        Assert.notNull(categoryView,"[rpc]远程调用album微服务的getCategoryView方法失败");

        //为AlbumInfoIndex对象的分类id属性赋值
        index.setCategory1Id(categoryView.getCategory1Id());
        index.setCategory2Id(categoryView.getCategory2Id());
        index.setCategory3Id(categoryView.getCategory3Id());

        //2.5 album_stat表
        Result<AlbumStatVo> albumStatVoResult = albumInfoFeignClient.getAlbumStatVoByAlbumId(albumId);
        AlbumStatVo albumStatVo = albumStatVoResult.getData();
        Assert.notNull(albumStatVo,"[rpc]远程调用album微服务的getAlbumStatVoByAlbumId方法失败");

        //获取四个维度的播放量、订阅量、购买量、评论量，并赋值给AlbumInfoIndex对象
        Integer playStatNum = albumStatVo.getPlayStatNum();
        index.setPlayStatNum(playStatNum);
        Integer subscribeStatNum = albumStatVo.getSubscribeStatNum();
        index.setSubscribeStatNum(subscribeStatNum);
        Integer buyStatNum = albumStatVo.getBuyStatNum();
        index.setBuyStatNum(buyStatNum);
        Integer commentStatNum = albumStatVo.getCommentStatNum();
        index.setCommentStatNum(commentStatNum);

        //2.5 热度值
        //Double hotScore = playStatNum * 0.4 + subscribeStatNum * 0.1 + buyStatNum * 0.3 + commentStatNum * 0.2;
        Double hotScore = new Random().nextDouble();
        index.setHotScore(hotScore);

        //3.存入es
        long endTime = System.currentTimeMillis();
        AlbumInfoIndex flag = albumInfoRepository.save(index);
        log.info("专辑:{}存储到ElasticSearch库中，状态:{},具体耗时:{}",albumId,flag != null ? "success" : "fail",endTime - startTime);

    }
    * */

}
